use std::cmp::max;

use super::cast::As;
use super::types::{Color, EcLevel, Version};

#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum Module {
  Empty,
  Masked(Color),
  Unmasked(Color),
}

impl From<Module> for Color {
  fn from(module: Module) -> Self {
    match module {
      Module::Empty => Color::Light,
      Module::Masked(c) | Module::Unmasked(c) => c,
    }
  }
}

impl Module {
  pub fn is_dark(self) -> bool {
    Color::from(self) == Color::Dark
  }

  pub fn mask(self, should_invert: bool) -> Self {
    match (self, should_invert) {
      (Module::Empty, true) => Module::Masked(Color::Dark),
      (Module::Empty, false) => Module::Masked(Color::Light),
      (Module::Unmasked(c), true) => Module::Masked(!c),
      (Module::Unmasked(c), false) | (Module::Masked(c), _) => {
        Module::Masked(c)
      }
    }
  }
}

#[derive(Clone)]
pub struct Canvas {
  width: i16,
  version: Version,
  ec_level: EcLevel,
  modules: Vec<Module>,
}

impl Canvas {
  pub fn new(version: Version, ec_level: EcLevel) -> Self {
    let width = version.width();
    Self {
      width,
      version,
      ec_level,
      modules: vec![Module::Empty; (width * width).as_usize()],
    }
  }

  fn coords_to_index(&self, x: i16, y: i16) -> usize {
    let x = if x < 0 { x + self.width } else { x }.as_usize();
    let y = if y < 0 { y + self.width } else { y }.as_usize();
    y * self.width.as_usize() + x
  }

  pub fn get(&self, x: i16, y: i16) -> Module {
    self.modules[self.coords_to_index(x, y)]
  }

  pub fn get_mut(&mut self, x: i16, y: i16) -> &mut Module {
    let index = self.coords_to_index(x, y);
    &mut self.modules[index]
  }

  pub fn put(&mut self, x: i16, y: i16, color: Color) {
    *self.get_mut(x, y) = Module::Masked(color);
  }
}

impl Canvas {
  fn draw_finder_pattern_at(&mut self, x: i16, y: i16) {
    let (dx_left, dx_right) = if x >= 0 { (-3, 4) } else { (-4, 3) };
    let (dy_top, dy_bottom) = if y >= 0 { (-3, 4) } else { (-4, 3) };
    for j in dy_top..=dy_bottom {
      for i in dx_left..=dx_right {
        self.put(
          x + i,
          y + j,
          match (i, j) {
            (4, _) | (_, 4) | (-4, _) | (_, -4) => Color::Light,
            (3, _) | (_, 3) | (-3, _) | (_, -3) => Color::Dark,
            (2, _) | (_, 2) | (-2, _) | (_, -2) => Color::Light,
            _ => Color::Dark,
          },
        );
      }
    }
  }

  fn draw_finder_patterns(&mut self) {
    self.draw_finder_pattern_at(3, 3);
    match self.version {
      Version::Micro(_) => {}
      Version::Normal(_) => {
        self.draw_finder_pattern_at(-4, 3);
        self.draw_finder_pattern_at(3, -4);
      }
    }
  }
}

impl Canvas {
  fn draw_alignment_pattern_at(&mut self, x: i16, y: i16) {
    if self.get(x, y) != Module::Empty {
      return;
    }
    for j in -2..=2 {
      for i in -2..=2 {
        self.put(
          x + i,
          y + j,
          match (i, j) {
            (2, _) | (_, 2) | (-2, _) | (_, -2) | (0, 0) => Color::Dark,
            _ => Color::Light,
          },
        );
      }
    }
  }

  fn draw_alignment_patterns(&mut self) {
    match self.version {
      Version::Micro(_) | Version::Normal(1) => {}
      Version::Normal(2..=6) => self.draw_alignment_pattern_at(-7, -7),
      Version::Normal(a) => {
        let positions = ALIGNMENT_PATTERN_POSITIONS[(a - 7).as_usize()];
        for x in positions.iter() {
          for y in positions.iter() {
            self.draw_alignment_pattern_at(*x, *y);
          }
        }
      }
    }
  }
}

static ALIGNMENT_PATTERN_POSITIONS: [&[i16]; 34] = [
  &[6, 22, 38],
  &[6, 24, 42],
  &[6, 26, 46],
  &[6, 28, 50],
  &[6, 30, 54],
  &[6, 32, 58],
  &[6, 34, 62],
  &[6, 26, 46, 66],
  &[6, 26, 48, 70],
  &[6, 26, 50, 74],
  &[6, 30, 54, 78],
  &[6, 30, 56, 82],
  &[6, 30, 58, 86],
  &[6, 34, 62, 90],
  &[6, 28, 50, 72, 94],
  &[6, 26, 50, 74, 98],
  &[6, 30, 54, 78, 102],
  &[6, 28, 54, 80, 106],
  &[6, 32, 58, 84, 110],
  &[6, 30, 58, 86, 114],
  &[6, 34, 62, 90, 118],
  &[6, 26, 50, 74, 98, 122],
  &[6, 30, 54, 78, 102, 126],
  &[6, 26, 52, 78, 104, 130],
  &[6, 30, 56, 82, 108, 134],
  &[6, 34, 60, 86, 112, 138],
  &[6, 30, 58, 86, 114, 142],
  &[6, 34, 62, 90, 118, 146],
  &[6, 30, 54, 78, 102, 126, 150],
  &[6, 24, 50, 76, 102, 128, 154],
  &[6, 28, 54, 80, 106, 132, 158],
  &[6, 32, 58, 84, 110, 136, 162],
  &[6, 26, 54, 82, 110, 138, 166],
  &[6, 30, 58, 86, 114, 142, 170],
];

impl Canvas {
  fn draw_line(
    &mut self,
    x1: i16,
    y1: i16,
    x2: i16,
    y2: i16,
    color_even: Color,
    color_odd: Color,
  ) {
    if y1 == y2 {
      for x in x1..=x2 {
        self.put(x, y1, if x % 2 == 0 { color_even } else { color_odd });
      }
    } else {
      for y in y1..=y2 {
        self.put(x1, y, if y % 2 == 0 { color_even } else { color_odd });
      }
    }
  }

  fn draw_timing_patterns(&mut self) {
    let width = self.width;
    let (y, x1, x2) = match self.version {
      Version::Micro(_) => (0, 8, width - 1),
      Version::Normal(_) => (6, 8, width - 9),
    };
    self.draw_line(x1, y, x2, y, Color::Dark, Color::Light);
    self.draw_line(y, x1, y, x2, Color::Dark, Color::Light);
  }
}

impl Canvas {
  fn draw_number(
    &mut self,
    number: u32,
    bits: u32,
    on_color: Color,
    off_color: Color,
    coords: &[(i16, i16)],
  ) {
    let mut mask = 1 << (bits - 1);
    for &(x, y) in coords {
      let color = if (mask & number) == 0 {
        off_color
      } else {
        on_color
      };
      self.put(x, y, color);
      mask >>= 1;
    }
  }
  fn draw_format_info_patterns_with_number(&mut self, format_info: u16) {
    let format_info = u32::from(format_info);
    match self.version {
      Version::Micro(_) => {
        self.draw_number(
          format_info,
          15,
          Color::Dark,
          Color::Light,
          &FORMAT_INFO_COORDS_MICRO_QR,
        );
      }
      Version::Normal(_) => {
        self.draw_number(
          format_info,
          15,
          Color::Dark,
          Color::Light,
          &FORMAT_INFO_COORDS_QR_MAIN,
        );
        self.draw_number(
          format_info,
          15,
          Color::Dark,
          Color::Light,
          &FORMAT_INFO_COORDS_QR_SIDE,
        );
        self.put(8, -8, Color::Dark);
      }
    }
  }

  fn draw_reserved_format_info_patterns(&mut self) {
    self.draw_format_info_patterns_with_number(0);
  }

  fn draw_version_info_patterns(&mut self) {
    match self.version {
      Version::Micro(_) | Version::Normal(1..=6) => {}
      Version::Normal(a) => {
        let version_info = VERSION_INFOS[(a - 7).as_usize()];
        self.draw_number(
          version_info,
          18,
          Color::Dark,
          Color::Light,
          &VERSION_INFO_COORDS_BL,
        );
        self.draw_number(
          version_info,
          18,
          Color::Dark,
          Color::Light,
          &VERSION_INFO_COORDS_TR,
        );
      }
    }
  }
}

static VERSION_INFO_COORDS_BL: [(i16, i16); 18] = [
  (5, -9),
  (5, -10),
  (5, -11),
  (4, -9),
  (4, -10),
  (4, -11),
  (3, -9),
  (3, -10),
  (3, -11),
  (2, -9),
  (2, -10),
  (2, -11),
  (1, -9),
  (1, -10),
  (1, -11),
  (0, -9),
  (0, -10),
  (0, -11),
];
static VERSION_INFO_COORDS_TR: [(i16, i16); 18] = [
  (-9, 5),
  (-10, 5),
  (-11, 5),
  (-9, 4),
  (-10, 4),
  (-11, 4),
  (-9, 3),
  (-10, 3),
  (-11, 3),
  (-9, 2),
  (-10, 2),
  (-11, 2),
  (-9, 1),
  (-10, 1),
  (-11, 1),
  (-9, 0),
  (-10, 0),
  (-11, 0),
];

static FORMAT_INFO_COORDS_QR_MAIN: [(i16, i16); 15] = [
  (0, 8),
  (1, 8),
  (2, 8),
  (3, 8),
  (4, 8),
  (5, 8),
  (7, 8),
  (8, 8),
  (8, 7),
  (8, 5),
  (8, 4),
  (8, 3),
  (8, 2),
  (8, 1),
  (8, 0),
];

static FORMAT_INFO_COORDS_QR_SIDE: [(i16, i16); 15] = [
  (8, -1),
  (8, -2),
  (8, -3),
  (8, -4),
  (8, -5),
  (8, -6),
  (8, -7),
  (-8, 8),
  (-7, 8),
  (-6, 8),
  (-5, 8),
  (-4, 8),
  (-3, 8),
  (-2, 8),
  (-1, 8),
];

static FORMAT_INFO_COORDS_MICRO_QR: [(i16, i16); 15] = [
  (1, 8),
  (2, 8),
  (3, 8),
  (4, 8),
  (5, 8),
  (6, 8),
  (7, 8),
  (8, 8),
  (8, 7),
  (8, 6),
  (8, 5),
  (8, 4),
  (8, 3),
  (8, 2),
  (8, 1),
];

static VERSION_INFOS: [u32; 34] = [
  0x07c94, 0x085bc, 0x09a99, 0x0a4d3, 0x0bbf6, 0x0c762, 0x0d847, 0x0e60d,
  0x0f928, 0x10b78, 0x1145d, 0x12a17, 0x13532, 0x149a6, 0x15683, 0x168c9,
  0x177ec, 0x18ec4, 0x191e1, 0x1afab, 0x1b08e, 0x1cc1a, 0x1d33f, 0x1ed75,
  0x1f250, 0x209d5, 0x216f0, 0x228ba, 0x2379f, 0x24b0b, 0x2542e, 0x26a64,
  0x27541, 0x28c69,
];

impl Canvas {
  pub fn draw_all_functional_patterns(&mut self) {
    self.draw_finder_patterns();
    self.draw_alignment_patterns();
    self.draw_reserved_format_info_patterns();
    self.draw_timing_patterns();
    self.draw_version_info_patterns();
  }
}

pub fn is_functional(version: Version, width: i16, x: i16, y: i16) -> bool {
  let x = if x < 0 { x + width } else { x };
  let y = if y < 0 { y + width } else { y };

  match version {
    Version::Micro(_) => x == 0 || y == 0 || (x < 9 && y < 9),
    Version::Normal(a) => {
      let non_alignment_test = x == 6
        || y == 6
        || (x < 9 && y < 9)
        || (x < 9 && y >= width - 8)
        || (x >= width - 8 && y < 9);
      if non_alignment_test {
        false
      } else if a == 1 {
        false
      } else if 2 <= a && a <= 6 {
        (width - 7 - x).abs() <= 2 && (width - 7 - y).abs() <= 2
      } else {
        let positions = ALIGNMENT_PATTERN_POSITIONS[(a - 7).as_usize()];
        let last = positions.len() - 1;
        for (i, align_x) in positions.iter().enumerate() {
          for (j, align_y) in positions.iter().enumerate() {
            if i == 0 && (j == 0 || j == last) || (i == last && j == 0) {
              continue;
            }
            if (*align_x - x).abs() <= 2 && (*align_y - y).abs() <= 2 {
              return true;
            }
          }
        }
        false
      }
    }
  }
}

struct DataModuleIter {
  x: i16,
  y: i16,
  width: i16,
  timing_pattern_column: i16,
}

impl DataModuleIter {
  fn new(version: Version) -> Self {
    let width = version.width();
    Self {
      x: width - 1,
      y: width - 1,
      width,
      timing_pattern_column: match version {
        Version::Micro(_) => 0,
        Version::Normal(_) => 6,
      },
    }
  }
}

impl Iterator for DataModuleIter {
  type Item = (i16, i16);
  fn next(&mut self) -> Option<Self::Item> {
    let adjusted_ref_col = if self.x <= self.timing_pattern_column {
      self.x + 1
    } else {
      self.x
    };

    if adjusted_ref_col <= 0 {
      return None;
    }
    let res = (self.x, self.y);
    let column_type = (self.width - adjusted_ref_col) % 4;
    match column_type {
      2 if self.y > 0 => {
        self.y -= 1;
        self.x += 1;
      }
      0 if self.y < self.width - 1 => {
        self.y += 1;
        self.x += 1;
      }
      0 | 2 if self.x == self.timing_pattern_column + 1 => {
        self.x -= 2;
      }
      _ => {
        self.x -= 1;
      }
    }
    Some(res)
  }
}

impl Canvas {
  fn draw_codewords<I>(
    &mut self,
    codewords: &[u8],
    is_half_codeword_at_end: bool,
    coords: &mut I,
  ) where
    I: Iterator<Item = (i16, i16)>,
  {
    let lenght = codewords.len();
    let last_word = if is_half_codeword_at_end {
      lenght - 1
    } else {
      lenght
    };
    for (i, b) in codewords.iter().enumerate() {
      let bits_end = if i == last_word { 4 } else { 0 };
      'outside: for j in (bits_end..=7).rev() {
        let color = if (*b & (1 << j)) == 0 {
          Color::Light
        } else {
          Color::Dark
        };
        while let Some((x, y)) = coords.next() {
          let r = self.get_mut(x, y);
          if *r == Module::Empty {
            *r = Module::Unmasked(color);
            continue 'outside;
          }
        }
        return;
      }
    }
  }

  pub fn draw_data(&mut self, data: &[u8], ec: &[u8]) {
    let is_half_codeword_at_end = match (self.version, self.ec_level) {
      (Version::Micro(1), EcLevel::L) | (Version::Micro(3), EcLevel::M) => true,
      _ => false,
    };
    let mut coords = DataModuleIter::new(self.version);
    self.draw_codewords(data, is_half_codeword_at_end, &mut coords);
    self.draw_codewords(ec, false, &mut coords)
  }
}

#[derive(Debug, Clone, Copy)]
pub enum MaskPattern {
  Checkerboard = 0b000,
  HorizontalLines = 0b001,
  VerticalLines = 0b010,
  DiagonalLines = 0b011,
  LargeCheckerboard = 0b100,
  Fields = 0b101,
  Diamonds = 0b110,
  Meadow = 0b111,
}

mod mask_funcs {
  pub fn checkerboard(x: i16, y: i16) -> bool {
    (x + y) % 2 == 0
  }

  pub fn horizontal_lines(_: i16, y: i16) -> bool {
    y % 2 == 0
  }

  pub fn vertical_lines(x: i16, _: i16) -> bool {
    x % 3 == 0
  }

  pub fn diagonal_lines(x: i16, y: i16) -> bool {
    (x + y) % 3 == 0
  }

  pub fn large_checkerboard(x: i16, y: i16) -> bool {
    (y / 2 + x / 3) % 2 == 0
  }

  pub fn fields(x: i16, y: i16) -> bool {
    (x * y) % 2 + (x * y) % 3 == 0
  }
  pub fn diamonds(x: i16, y: i16) -> bool {
    ((x * y) % 2 + (x * y) % 3) % 2 == 0
  }
  pub fn meadow(x: i16, y: i16) -> bool {
    ((x + y) % 2 + (x * y) % 3) % 2 == 0
  }
}

fn get_mask_fn(pattern: MaskPattern) -> fn(i16, i16) -> bool {
  match pattern {
    MaskPattern::Checkerboard => mask_funcs::checkerboard,
    MaskPattern::HorizontalLines => mask_funcs::horizontal_lines,
    MaskPattern::VerticalLines => mask_funcs::vertical_lines,
    MaskPattern::DiagonalLines => mask_funcs::diagonal_lines,
    MaskPattern::Diamonds => mask_funcs::diamonds,
    MaskPattern::LargeCheckerboard => mask_funcs::large_checkerboard,
    MaskPattern::Fields => mask_funcs::fields,
    MaskPattern::Meadow => mask_funcs::meadow,
  }
}

impl Canvas {
  pub fn apply_mask(&mut self, pattern: MaskPattern) {
    let mask_fn = get_mask_fn(pattern);
    for x in 0..self.width {
      for y in 0..self.width {
        let module = self.get_mut(x, y);
        *module = module.mask(mask_fn(x, y));
      }
    }
    self.draw_format_info_patterns(pattern);
  }

  fn draw_format_info_patterns(&mut self, pattern: MaskPattern) {
    let format_number = match self.version {
      Version::Normal(_) => {
        let simple_format_number =
          ((self.ec_level as usize) ^ 1) << 3 | (pattern as usize);
        FORMAT_INFOS_QR[simple_format_number]
      }
      Version::Micro(a) => {
        let micro_pattern_number = match pattern {
          MaskPattern::HorizontalLines => 0b00,
          MaskPattern::LargeCheckerboard => 0b01,
          MaskPattern::Diamonds => 0b10,
          MaskPattern::Meadow => 0b11,
          _ => panic!("Unsupported mask pattern in Micro QR code"),
        };

        let symbol_number = match (a, self.ec_level) {
          (1, EcLevel::L) => 0b000,
          (2, EcLevel::L) => 0b001,
          (2, EcLevel::M) => 0b010,
          (3, EcLevel::L) => 0b011,
          (3, EcLevel::M) => 0b100,
          (4, EcLevel::L) => 0b101,
          (4, EcLevel::M) => 0b110,
          (4, EcLevel::Q) => 0b111,
          _ => panic!(
            "Unsupported version / ec_level combination in Micro QR code"
          ),
        };
        let simple_format_number = symbol_number << 2 | micro_pattern_number;
        FORMAT_INFOS_MICRO_QR[simple_format_number]
      }
    };
    self.draw_format_info_patterns_with_number(format_number);
  }
}

static FORMAT_INFOS_QR: [u16; 32] = [
  0x5412, 0x5125, 0x5e7c, 0x5b4b, 0x45f9, 0x40ce, 0x4f97, 0x4aa0, 0x77c4,
  0x72f3, 0x7daa, 0x789d, 0x662f, 0x6318, 0x6c41, 0x6976, 0x1689, 0x13be,
  0x1ce7, 0x19d0, 0x0762, 0x0255, 0x0d0c, 0x083b, 0x355f, 0x3068, 0x3f31,
  0x3a06, 0x24b4, 0x2183, 0x2eda, 0x2bed,
];

static FORMAT_INFOS_MICRO_QR: [u16; 32] = [
  0x4445, 0x4172, 0x4e2b, 0x4b1c, 0x55ae, 0x5099, 0x5fc0, 0x5af7, 0x6793,
  0x62a4, 0x6dfd, 0x68ca, 0x7678, 0x734f, 0x7c16, 0x7921, 0x06de, 0x03e9,
  0x0cb0, 0x0987, 0x1735, 0x1202, 0x1d5b, 0x186c, 0x2508, 0x203f, 0x2f66,
  0x2a51, 0x34e3, 0x31d4, 0x3e8d, 0x3bba,
];

impl Canvas {
  fn compute_adjacent_penalty_score(&self, is_horizontal: bool) -> u16 {
    let mut total = 0;
    for i in 0..self.width {
      let map_fn = |j| {
        if is_horizontal {
          self.get(j, i)
        } else {
          self.get(i, j)
        }
      };
      let colors = (0..self.width)
        .map(map_fn)
        .chain(Some(Module::Empty).into_iter());
      let mut last_color = Module::Empty;
      let mut consecutive_len = 1u16;
      for color in colors {
        if color == last_color {
          consecutive_len += 1;
        } else {
          last_color = color;
          if consecutive_len >= 5 {
            total += consecutive_len - 2;
          }
          consecutive_len = 1;
        }
      }
    }
    total
  }

  fn compute_block_penalty_score(&self) -> u16 {
    let mut total = 0;
    for i in 0..self.width - 1 {
      for j in 0..self.width - 1 {
        let this = self.get(i, j);
        let right = self.get(i + 1, j);
        let bottom = self.get(i, j + 1);
        let bottom_right = self.get(i + 1, j + 1);
        if this == right && right == bottom && bottom == bottom_right {
          total += 3;
        }
      }
    }
    total
  }

  fn compute_finder_penalty_score(&self, is_horizontal: bool) -> u16 {
    static PATTERN: [Color; 7] = [
      Color::Dark,
      Color::Light,
      Color::Dark,
      Color::Dark,
      Color::Dark,
      Color::Light,
      Color::Dark,
    ];
    let mut total = 0;

    for i in 0..self.width {
      for j in 0..self.width - 6 {
        let get: Box<dyn Fn(i16) -> Color> = if is_horizontal {
          Box::new(|k| self.get(k, i).into())
        } else {
          Box::new(|k| self.get(i, k).into())
        };

        if (j..(j + 7)).map(&*get).ne(PATTERN.iter().cloned()) {
          continue;
        }

        let check = |k| 0 <= k && k < self.width && get(k) != Color::Light;
        if !((j - 4)..j).any(&check) || !((j + 7)..(j + 11)).any(&check) {
          total += 40;
        }
      }
    }

    total - 360
  }

  fn compute_balance_penalty_score(&self) -> u16 {
    let dark_modules = self.modules.iter().filter(|m| m.is_dark()).count();
    let total = self.modules.len();
    let ratio = dark_modules * 200 / total;
    if ratio >= 100 {
      ratio - 100
    } else {
      100 - ratio
    }
    .as_u16()
  }

  fn compute_light_side_penalty_score(&self) -> u16 {
    let h = (1..self.width)
      .filter(|j| !self.get(*j, -1).is_dark())
      .count();
    let v = (1..self.width)
      .filter(|j| !self.get(-1, *j).is_dark())
      .count();
    (h + v + 15 * max(h, v)).as_u16()
  }
  fn compute_total_penalty_scores(&self) -> u16 {
    match self.version {
      Version::Normal(_) => {
        let s1_a = self.compute_adjacent_penalty_score(true);
        let s1_b = self.compute_adjacent_penalty_score(false);
        let s2 = self.compute_block_penalty_score();
        let s3_a = self.compute_finder_penalty_score(true);
        let s3_b = self.compute_finder_penalty_score(false);
        let s4 = self.compute_balance_penalty_score();
        s1_a + s1_b + s2 + s3_a + s3_b + s4
      }
      Version::Micro(_) => self.compute_light_side_penalty_score(),
    }
  }
}

static ALL_PATTERNS_QR: [MaskPattern; 8] = [
  MaskPattern::Checkerboard,
  MaskPattern::HorizontalLines,
  MaskPattern::VerticalLines,
  MaskPattern::DiagonalLines,
  MaskPattern::LargeCheckerboard,
  MaskPattern::Fields,
  MaskPattern::Diamonds,
  MaskPattern::Meadow,
];

static ALL_PATTERNS_MICRO_QR: [MaskPattern; 4] = [
  MaskPattern::HorizontalLines,
  MaskPattern::LargeCheckerboard,
  MaskPattern::Diamonds,
  MaskPattern::Meadow,
];

impl Canvas {
  pub fn apply_best_mask(&self) -> Self {
    match self.version {
      Version::Normal(_) => ALL_PATTERNS_QR.iter(),
      Version::Micro(_) => ALL_PATTERNS_MICRO_QR.iter(),
    }
    .map(|pattern| {
      let mut c = self.clone();
      c.apply_mask(*pattern);
      c
    })
    .min_by_key(Self::compute_total_penalty_scores)
    .expect("at least one pattern")
  }

  pub fn to_bools(&self) -> Vec<bool> {
    self.modules.iter().map(|m| m.is_dark()).collect()
  }

  pub fn into_colors(self) -> Vec<Color> {
    self.modules.into_iter().map(Color::from).collect()
  }
}
